Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve TextTrimming customization experience #16521

Merged
merged 3 commits into from
Aug 1, 2024

Conversation

Gillibald
Copy link
Contributor

What does the pull request do?

This PR changes the visibility of some helper functions from internal to public to make the process of adding a custom TextTrimming implementation easier.

What is the current behavior?

What is the updated/expected behavior with this PR?

How was the solution implemented (if it's not obvious)?

Checklist

Breaking changes

Obsoletions / Deprecations

Fixed issues

Sample

<Window.Styles>
   <Style Selector="Grid.demo">
     <Style Selector="^ Label">
       <Setter Property="VerticalAlignment" Value="Center"/>
       <Setter Property="Margin" Value="0,0,4,0"/>
     </Style>

     <Style Selector="^ Border">
       <Setter Property="VerticalAlignment" Value="Center"/>
       <Setter Property="BorderBrush" Value="Black"/>
       <Setter Property="BorderThickness" Value="1"/>
     </Style>

     <Style Selector="^ TextBlock">
       <Setter Property="VerticalAlignment" Value="Center"/>
     </Style>
   </Style>
 </Window.Styles>



 <Grid ColumnDefinitions="Auto,150"
       RowDefinitions="Auto,Auto,Auto,Auto, Auto"
       Margin="10"
       Classes="demo">
   <!-- Row 0 -->

   <Label Content="1. Text with CharacterEllipsis"
          Grid.Row="0" Grid.Column="0"/>
   <Border Grid.Row="0" Grid.Column="1">
     <TextBlock Text="Some very long text that should be trimmed."
                TextTrimming="CharacterEllipsis"/>
   </Border>

   <!-- Row 1 -->
   <Label Content="2. Text with WordEllipsis"
          Grid.Row="1" Grid.Column="0"/>
   <Border Grid.Row="1" Grid.Column="1">
     <TextBlock Text="Some very long text that should be trimmed."
                TextTrimming="WordEllipsis"/>
   </Border>

   <!-- Row 2 -->
   <Label Content="3. Path with CharacterEllipsis"
          Grid.Row="2" Grid.Column="0"/>
   <Border Grid.Row="2" Grid.Column="1">
     <TextBlock Text="C:\Some\very\long\text\that\should\be\trimmed."
                TextTrimming="CharacterEllipsis"/>
   </Border>

   <!-- Row 3 -->
   <Label Content="4. Path with WordEllipsis"
          Grid.Row="3" Grid.Column="0"/>
   <Border Grid.Row="3" Grid.Column="1">
     <TextBlock Text="C:\Some\very\long\text\that\should\be\trimmed."
                TextTrimming="WordEllipsis"/>
   </Border>

   <!-- Row 4 -->
   <Label Content="5. Path with custom trimming"
          Grid.Row="4" Grid.Column="0"/>
   <Border Grid.Row="4" Grid.Column="1">
     <TextBlock Text="C:\Some\very\long\text\that\should\be\trimmed.">
       <TextBlock.TextTrimming>
         <local:CustomTextTrimming/>
       </TextBlock.TextTrimming>
     </TextBlock>
   </Border>
 </Grid>

CustomTextTrimming implementation

public class CustomTextTrimming : TextTrimming
    {
        public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)
        {
            return new CustomTrailingWordEllipsis("\u2026", createInfo.Width, createInfo.TextRunProperties,
                createInfo.FlowDirection);
        }

        private class CustomTrailingWordEllipsis : TextCollapsingProperties
        {
            public CustomTrailingWordEllipsis(
                string ellipsis,
                double width,
                TextRunProperties textRunProperties,
                FlowDirection flowDirection
            )
            {
                Width = width;
                Symbol = new TextCharacters(ellipsis, textRunProperties);
                FlowDirection = flowDirection;
            }

            /// <inheritdoc/>
            public override double Width { get; }

            /// <inheritdoc/>
            public override TextRun Symbol { get; }

            public override FlowDirection FlowDirection { get; }

            /// <inheritdoc />
            public override TextRun[]? Collapse(TextLine textLine)
            {
                return Collapse(textLine, this, true);
            }

            private static TextRun[] Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
            {
                var textRuns = textLine.TextRuns;

                if (textRuns.Count == 0)
                {
                    return null;
                }

                var runIndex = 0;
                var currentWidth = 0.0;
                var collapsedLength = 0;
                var shapedSymbol = TextFormatter.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight);

                if (properties.Width < shapedSymbol.GlyphRun.Bounds.Width)
                {
                    //Not enough space to fit in the symbol
                    return [];
                }

                var availableWidth = properties.Width - shapedSymbol.Size.Width;

                if (properties.FlowDirection == FlowDirection.LeftToRight)
                {
                    while (runIndex < textRuns.Count)
                    {
                        var currentRun = textRuns[runIndex];

                        switch (currentRun)
                        {
                            case ShapedTextRun shapedRun:
                                {
                                    currentWidth += shapedRun.Size.Width;

                                    if (currentWidth > availableWidth)
                                    {
                                        if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
                                        {
                                            if (isWordEllipsis && measuredLength < textLine.Length)
                                            {
                                                var currentBreakPosition = 0;

                                                var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);

                                                while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak))
                                                {
                                                    var nextBreakPosition = lineBreak.PositionMeasure;

                                                    if (nextBreakPosition == 0)
                                                    {
                                                        break;
                                                    }

                                                    if (nextBreakPosition >= measuredLength)
                                                    {
                                                        break;
                                                    }

                                                    currentBreakPosition = nextBreakPosition;
                                                }

                                                var codepoint = Codepoint.ReadAt(currentRun.Text.Span, currentBreakPosition, out _);

                                                //Handle strings that might look like a file path here
                                                if (codepoint.Value == '\\')
                                                {
                                                    var graphemeEnumerator =
                                                        new GraphemeEnumerator(
                                                            currentRun.Text.Span.Slice(currentBreakPosition));

                                                    var lengthInPath = 0;
                                                    var breakPositionInPath = 0;

                                                    while (graphemeEnumerator.MoveNext(out var grapheme))
                                                    {
                                                        if (currentBreakPosition + lengthInPath + grapheme.Length >= measuredLength)
                                                        {
                                                            break;
                                                        }

                                                        lengthInPath += grapheme.Length;

                                                        if (grapheme.FirstCodepoint.Value is '\\')
                                                        {
                                                            breakPositionInPath = lengthInPath;
                                                        }
                                                    }

                                                    currentBreakPosition += breakPositionInPath - 1;
                                                }

                                                measuredLength = currentBreakPosition;
                                            }
                                        }

                                        collapsedLength += measuredLength;

                                        return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol);
                                    }

                                    availableWidth -= shapedRun.Size.Width;

                                    break;
                                }

                            case DrawableTextRun drawableRun:
                                {
                                    //The whole run needs to fit into available space
                                    if (currentWidth + drawableRun.Size.Width > availableWidth)
                                    {
                                        return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol);
                                    }

                                    availableWidth -= drawableRun.Size.Width;

                                    break;
                                }
                        }

                        collapsedLength += currentRun.Length;

                        runIndex++;
                    }
                }
                else
                {
                    runIndex = textRuns.Count - 1;

                    while (runIndex >= 0)
                    {
                        var currentRun = textRuns[runIndex];

                        switch (currentRun)
                        {
                            case ShapedTextRun shapedRun:
                                {
                                    currentWidth += shapedRun.Size.Width;

                                    if (currentWidth > availableWidth)
                                    {
                                        if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
                                        {
                                            if (isWordEllipsis && measuredLength < textLine.Length)
                                            {
                                                var currentBreakPosition = 0;

                                                var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);

                                                while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak))
                                                {
                                                    var nextBreakPosition = lineBreak.PositionMeasure;

                                                    if (nextBreakPosition == 0)
                                                    {
                                                        break;
                                                    }

                                                    if (nextBreakPosition >= measuredLength)
                                                    {
                                                        break;
                                                    }

                                                    currentBreakPosition = nextBreakPosition;
                                                }

                                                measuredLength = currentBreakPosition;
                                            }
                                        }

                                        collapsedLength += measuredLength;

                                        return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol);
                                    }

                                    availableWidth -= shapedRun.Size.Width;

                                    break;
                                }

                            case DrawableTextRun drawableRun:
                                {
                                    //The whole run needs to fit into available space
                                    if (currentWidth + drawableRun.Size.Width > availableWidth)
                                    {
                                        return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol);
                                    }

                                    availableWidth -= drawableRun.Size.Width;

                                    break;
                                }
                        }

                        collapsedLength += currentRun.Length;

                        runIndex--;
                    }
                }

                return null;
            }
        }
    }

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 11.2.999-cibuild0050600-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@Gillibald Gillibald added this pull request to the merge queue Jul 31, 2024
Merged via the queue into AvaloniaUI:master with commit a562c4e Aug 1, 2024
10 checks passed
@Gillibald Gillibald deleted the fixes/pathWordWrap branch August 1, 2024 02:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants